package agenda;
import excecoes.DuracaoInvalida;
import excecoes.HorarioInvalido;
import excecoes.DataInvalida;

import java.io.Serializable;
import java.util.*;

public class Evento implements Comparable<Evento>, Serializable {

	private static final long serialVersionUID = 1L;

	public static final int POSICAO_DIA = 0, POSICAO_MES = 1, POSICAO_ANO = 2;
	private final int POSICAO_HORA = 0, POSICAO_MINUTO = 1;

	private final double MARGEM_DE_ERRO = 2.00;

	private String nome, duracao, descricao, tipo;
	private Calendar dataInicio, dataTermino;
	private List<Contato> participantes;


	/**
	 * Cria um evento.
	 * @param nome O nome do evento.
	 * @param dataInicio A data de inicio do evento, no formato dd/MM/aaaa
	 * @param horarioInicio O horario de inicio do evento.
	 * @param duracao A duracao do evento, no formato hh:mm
	 * @param descricao Uma descricao para o evento.
	 * @param tipo O tipo do evento.
	 * @throws DataInvalida Lanca excecao de DataInvalida caso a data informada seja invalida
	 * ou nao siga o formato especificado.
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso o horario informado seja invalido
	 * ou nao siga o formato especificado.
	 */

	public Evento(String nome, String dataInicio, String horario, String duracao, String descricao, String tipo)
			throws DataInvalida, HorarioInvalido {

		this.nome = nome;
		this.duracao = duracao;
		this.descricao = descricao;
		this.tipo = tipo;

		this.dataInicio = new GregorianCalendar();
		setDataInicio(dataInicio);
		setHorarioInicio(horario);

		this.dataTermino = new GregorianCalendar();
		setDataTermino(duracao);

		participantes = new ArrayList<Contato>();
	}

	/**
	 * Cria um evento.
	 * @param nome O nome do evento.
	 * @param dataInicio A data de inicio do evento, no formato dd/MM/aaaa
	 * @param horarioInicio O horario de inicio do evento.
	 * @param duracao A duracao do evento, no formato hh:mm
	 * @param descricao Uma descricao para o evento.
	 * @throws DataInvalida Lanca excecao de DataInvalida caso a data informada seja invalida
	 * ou nao siga o formato especificado.
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso o horario informado seja invalido
	 * ou nao siga o formato especificado.
	 */

	public Evento(String nome, String dataInicio, String horarioInicio, String duracao, String descricao)
			throws DataInvalida, HorarioInvalido {

		this(nome, dataInicio, horarioInicio, duracao, descricao, null);
	}


	/**
	 * Recupera o nome do evento.
	 * @return O nome do evento.
	 */

	public String getNome() {
		return nome;
	}

	/**
	 * Atribui um novo nome ao evento.
	 * @param nome O novo nome do evento.
	 */

	public void setNome(String nome) {
		this.nome = nome;
	}

	/**
	 * Recupera a data de inicio do evento.
	 * @return A data de inicio do evento, como objeto GregorianCalendar.
	 */

	public GregorianCalendar getDataInicio() {
		return (GregorianCalendar)dataInicio;
	}

	/**
	 * Recupera a data de inicio do evento
	 * @return Uma String no formato dd/MM/aaaa que representa a data de inicio do evento
	 */

	public String getDataInicioFormatada() {
		return String.format("%02d/%02d/%04d", dataInicio.get(Calendar.DAY_OF_MONTH), dataInicio.get(Calendar.MONTH) + 1, dataInicio.get(Calendar.YEAR));
	}

	/**
	 * Atribui uma nova data de inicio ao evento
	 * @param novaData A nova data de inicio do evento, no formato dd/MM/aaaa
	 * @throws DataInvalida Lanca excecao de DataInvalida caso a data informada seja invalida
	 * ou nao siga o formato especificado
	 */

	public void setDataInicio(String novaData) throws DataInvalida {
		validaData(novaData);

		String[] dataQuebrada = novaData.split("/");

		int dia = Integer.parseInt(dataQuebrada[POSICAO_DIA]);
		int mes = Integer.parseInt(dataQuebrada[POSICAO_MES]) - 1;
		int ano = Integer.parseInt(dataQuebrada[POSICAO_ANO]);

		validaAno(dataInicio, ano);
		dataInicio.set(Calendar.YEAR, ano);

		validaMes(dataInicio, mes);
		dataInicio.set(Calendar.MONTH, mes);

		validaDia(dataInicio, dia);
		dataInicio.set(Calendar.DAY_OF_MONTH, dia);
	}

	/**
	 * Verifica se uma data se enquadra no formato dd/MM/aaaa
	 * @param data A data a ser testada
	 * @throws DataInvalida Lanca excecao de DataInvalida caso a data nao siga o formato especificado
	 */

	public static void validaData(String data) throws DataInvalida {
		if (data.length() != 10) {
			throw new DataInvalida("Data invalida! Use o formato dd/MM/aaaa");
		}

		if (data.charAt(2) != '/' || data.charAt(5) != '/') {
			throw new DataInvalida("Data invalida! Use o formato dd/MM/aaaa");
		}

		for (int i = 0; i < data.length(); i++) {
			if (i != 2 && i != 5) {
				if (!Character.isDigit(data.charAt(i))) {
					throw new DataInvalida("Data invalida! Use o formato dd/MM/aaaa");
				}
			}
		}
	}

	/**
	 * Verifica se um ano e valido
	 * @param data A data que contem esse ano
	 * @param ano O ano a ser verificado
	 * @throws DataInvalida Lanca excecao de DataInvalida caso e ano seja invalido
	 */

	public static void validaAno(Calendar data, int ano) throws DataInvalida {
		if (ano < data.getActualMinimum(Calendar.YEAR) || ano > data.getActualMaximum(Calendar.YEAR)) {
			throw new DataInvalida("Ano invalido!");
		}
	}

	/**
	 * Verifica se um mes e valido
	 * @param data A data que contem esse mes
	 * @param mes O mes a ser verificado
	 * @throws DataInvalida Lanca excecao de DataInvalida caso o mes seja invalido
	 */

	public static void validaMes(Calendar data, int mes) throws DataInvalida {
		if (mes < data.getActualMinimum(Calendar.MONTH) || mes > data.getActualMaximum(Calendar.MONTH)) {
			throw new DataInvalida("Mes invalido!");
		}
	}

	/**
	 * Verifica se um dia e valido. A validade de um dia depende do mes corrente.
	 * @param data A data que contem esse dia.
	 * @param dia O dia a ser verificado
	 * @throws DataInvalida Lanca excecao de DataInvalida caso o dia seja invalido
	 */

	public static void validaDia(Calendar data, int dia) throws DataInvalida {
		if (dia < data.getActualMinimum(Calendar.DAY_OF_MONTH) || dia > data.getActualMaximum(Calendar.DAY_OF_MONTH)) {
			throw new DataInvalida("Dia invalido!");
		}
	}

	/**
	 * Recupera o horario de inicio do evento
	 * @return Uma String no formato hh:mm que representa o horario de inicio do evento
	 */

	public String getHorarioInicioFormatado() {
		return String.format("%02d:%02d", dataInicio.get(Calendar.HOUR_OF_DAY), dataInicio.get(Calendar.MINUTE));
	}

	/**
	 * Atibui um novo horario de inicio ao Evento
	 * @param horario O novo horario de inicio, no formato hh:mm
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso o horario informado seja invalido
	 * ou nao siga o formato especificado
	 */

	public void setHorarioInicio(String horario) throws HorarioInvalido {
		String mensagem = "Horario invalido! Use o formato hh:mm";
		validaHorario(horario, mensagem);

		String[] horarioQuebrado = horario.split(":");

		int minuto  = Integer.parseInt(horarioQuebrado[POSICAO_MINUTO]);
		int hora = Integer.parseInt(horarioQuebrado[POSICAO_HORA]);

		validaHora(hora);
		dataInicio.set(Calendar.HOUR_OF_DAY, hora);

		validaMinuto(minuto);
		dataInicio.set(Calendar.MINUTE, minuto);

	}

	/**
	 * Verifica se um horario se enquadra no formato hh:mm
	 * @param horario O horario a ser verificado
	 * @param mensagem A mensagem de erro gerada caso o horario seja invalido
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso o horario nao siga o formato especificado
	 */

	public void validaHorario(String horario, String mensagem) throws HorarioInvalido {
		// hh:mm

		if (horario.length() != 5 || horario.charAt(2) != ':') {
			throw new HorarioInvalido(mensagem);
		}

		for (int i = 0; i < horario.length(); i++) {
			if (i != 2) {
				if (!Character.isDigit(horario.charAt(i))) {
					throw new HorarioInvalido(mensagem);
				}
			}
		}
	}

	/**
	 * Verifica se uma hora e valida. Uma hora deve ser um inteiro maior ou igual a 0 e menor que 24
	 * @param hora A hora a ser verificada
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso a hora seja invalida
	 */

	public void validaHora(int hora) throws HorarioInvalido {
		if (hora < dataInicio.getActualMinimum(Calendar.HOUR_OF_DAY) || hora > dataInicio.getActualMaximum(Calendar.HOUR_OF_DAY)) {
			throw new HorarioInvalido("Hora invalida!");
		}
	}

	/**
	 * Verifica se um minuto e valido. Um minuto deve ser um inteiro maior ou igual a 0 e menor que 60
	 * @param minuto O minuto a ser verificado
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso o minuto seja invalido
	 */

	public void validaMinuto(int minuto) throws HorarioInvalido {
		if (minuto < dataInicio.getActualMinimum(Calendar.MINUTE) || minuto > dataInicio.getActualMaximum(Calendar.MINUTE)) {
			throw new HorarioInvalido("Minuto invalido!");
		}
	}

	/**
	 * Recupera a data de termino do evento
	 * @return Um objeto GregorianCalendar que guarda a data de termino do evento
	 */

	public GregorianCalendar getDataTermino() {
		return (GregorianCalendar)dataTermino;
	}

	/**
	 * Atribui uma nova data de termino ao evento com base na sua data de inicio e na duracao informada
	 * @param duracao A duracao do evento, no formato hh:mm
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso a duracao seja invalida
	 */

	public void setDataTermino(String duracao) throws HorarioInvalido {
		this.dataTermino = (GregorianCalendar)dataInicio.clone();
		validaDuracao(duracao);

		String[] duracaoQuebrada = duracao.split(":");

		int horas = Integer.parseInt(duracaoQuebrada[POSICAO_HORA]);
		int minutos = Integer.parseInt(duracaoQuebrada[POSICAO_MINUTO]);

		validaHoras(horas);
		validaMinutos(horas, minutos);

		dataTermino.add(Calendar.MINUTE, minutos);
		dataTermino.add(Calendar.HOUR_OF_DAY, horas);

	}

	/**
	 * Verifica se uma duracao se enquadra no formato hh:mm
	 * @param duracao A duracao a ser verificada
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso a duracao seja invalida
	 */

	public void validaDuracao(String duracao) throws HorarioInvalido {
		String mensagem = "Duracao invalida! Use o formato hh:mm";
		validaHorario(duracao, mensagem);
	}

	/**
	 * Verifica a validade de uma quantidade de horas (de uma duracao). A quantidade de horas deve ser um inteiro nao negativo.
	 * @param horas A quantidade de horas a ser verificada.
	 * @throws DuracaoInvalida Lanca excecao de DuracaoInvalida caso a quantidade de horas seja invalida.
	 */

	public void validaHoras(int horas) throws DuracaoInvalida {
		if (horas < 0) {
			throw new DuracaoInvalida("Quantidade de horas invalida!");
		}
	}

	/**
	 * Verifica a validade de uma quantidade de minutos (de uma duracao). A quantidade de minutos deve ser um inteiro nao negativo e
	 * nao deve exceder 59. Se a quantidade de horas for 0, a quantidade de minutos deve ser estritamente positiva.
	 * @param horas A quantidade de horas.
	 * @param minutos A quantidade de minutos a ser verificada.
	 * @throws DuracaoInvalida Lanca excecao de DuracaoInvalida caso a quantidade de minutos seja invalida.
	 */

	public void validaMinutos(int horas, int minutos) throws DuracaoInvalida {
		if ((minutos < 0 || minutos > 59) || (horas == 0 && minutos == 0)) {
			throw new DuracaoInvalida("Quantidade de minutos invalida!");
		}
	}

	/**
	 * Recupera a duracao no formato hh:mm.
	 * @return Uma String que contem a duracao do Evento.
	 */

	public String getDuracao() {
		return String.format("%02d horas e %02d minutos", Integer.parseInt(duracao.split(":")[0]), Integer.parseInt(duracao.split(":")[1]));
	}

	/**
	 * Atribui uma nova duracao ao Evento. A duracao deve seguir o formato hh:mm;
	 * @param duracao A nova duracao do Evento.
	 * @throws HorarioInvalido Lanca excecao de HorarioInvalido caso a duracao seja invalida.
	 */

	public void setDuracao(String duracao) throws HorarioInvalido {
		setDataTermino(duracao);
		this.duracao = duracao;
	}

	/**
	 * Recupera a descricao do Evento.
	 * @return A descricao do Evento.
	 */

	public String getDescricao() {
		return descricao;
	}

	/**
	 * Atribui uma nova descricao ao Evento.
	 * @param descricao A nova descricao do Evento.
	 */

	public void setDescricao(String descricao) {
		this.descricao = descricao;
	}

	/**
	 * Recupera o tipo do Evento.
	 * @return O tipo do Evento.
	 */

	public String getTipo() {
		return tipo;
	}

	/**
	 * Atribui um novo tipo ao Evento.
	 * @param tipo O novo tipo do Evento.
	 */

	public void setTipo(String tipo) {
		this.tipo = tipo;
	}

	/**
	 * Recupera a lista de contatos participantes deste Evento.
	 * @return A lista de contatos participantes deste Evento.
	 */

	public List<Contato> getParticipantes() {
		return participantes;
	}

	/**
	 * Adiciona um contato a lista de contatos que participam deste evento.
	 * @param c O contato a ser adicionado.
	 */

	public void addParticipante(Contato c) {
		int posicao = Agenda.encontraPosicao(c, (ArrayList<Contato>)participantes);
		participantes.add(posicao, c);
	}

	/**
	 * Verifica se ha conflito de horario entre dois eventos.
	 * @param ev O evento com o qual o conflito sera checado.
	 * @return true, se houver conflito de horario e false caso contario. Se um evento termina no exato
	 * instante em que outro come�a, nao ha conflito de horario.
	 */

	public boolean intersecta(Evento ev) {
		/*
		if (dataInicio.compareTo(ev.getDataInicio()) == 0) {
			System.out.println("data inicio iguais");
			return true;
		} else if (dataInicio.compareTo(ev.getDataInicio()) > 0 && dataInicio.compareTo(ev.getDataTermino()) < 0) {
			System.out.println("dt ini 1 > dt ini 2 " + (dataInicio.compareTo(ev.getDataInicio()) > 0));
			System.out.println("dt ini 1 < dt ter 2 " + (dataInicio.compareTo(ev.getDataTermino()) < 0) );
			System.out.println("aguia 2");
			return true;
		} else {
			return false;
		}
		*/
		return ((dataInicio.compareTo(ev.getDataInicio()) == 0)
				|| (dataInicio.compareTo(ev.getDataInicio()) > 0 && dataInicio.compareTo(ev.getDataTermino()) < 0) );
	}

	/**
	 * Compara dois eventos.
	 * @param ev Um Evento a ser comparado com este.
	 * @return Zero se os dois eventos comecam ao mesmo tempo, um valor menor que zero se o primeiro evento
	 * comeca antes do segundo ou um valor maior que zero caso contrario.
	 */

	@Override
	public int compareTo(Evento ev) {
		return comparaDatas(dataInicio, ev.getDataInicio());
	}


	/**
	 * Compara duas datas representadas em objetos Calendar.
	 * @param d1 A primeira data.
	 * @param d2 A segunda data.
	 * @return Zero se as duas datas podem ser consideradas iguais, um valor menor que zero se a primeira
	 * data antecede a segunda e um valor maior que zero caso contrario.
	 */

	public int comparaDatas(Calendar d1, Calendar d2) {

		long difference = d1.getTimeInMillis() - d2.getTimeInMillis();

		if (Math.abs(difference) < MARGEM_DE_ERRO) {
			return 0;
		} else if (difference < 0) {
			return -1;
		} else return 1;
	}

	/**
	 * Verficia se um dos nomes do evento contem a String passada como parametro.
	 * @param parteDoNome Uma parte do nome do evento.
	 * @return true caso algum nome do evento contenha a parte do nome dada e false caso contrario.
	 */

	public boolean nomeContem (String parteDoNome) {

		for (String n : nome.split(" ")) {
			if (n.contains(parteDoNome)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Representa um Evento como String.
	 * @return A representacao em String de um Evento.
	 */

	@Override
	public String toString() {

		String result = "Evento " + getNome() + "\n";
		result += "Data de inicio: " + getDataInicioFormatada() + "\n";
		result += "Horario de inicio: " + getHorarioInicioFormatado() + "\n";
		result += "Duracao: " + getDuracao() + "\n";
		result += "Descricao: " + getDescricao() + "\n";

		if (getTipo() != null) {
			result += "Tipo: " + getTipo() + "\n";
		}

		if (participantes.size() == 0) {
			result += "\nNao ha contatos cadastrados neste evento.";
		} else {
			result += "\nContatos cadastrados neste evento:";
			for (Contato c : participantes) {
				result += "\n" + c.getNome();
			}
		}

		return result;
	}
}
